Skip to content

feat(types): declare a gantt bar as an object on both published faces - #8405

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-7365-gantt-bar-object-declaration
Sep 7, 2026
Merged

feat(types): declare a gantt bar as an object on both published faces#8405
os-zhuang merged 4 commits into
mainfrom
claude/issue-7365-gantt-bar-object-declaration

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #7365

A gantt BAR is now declared an OBJECT on both published faces. An authored
null bar is refused at validate, by its own name, at items[i].items[j].

What was wrong

TimelineSchema.items[].items[] — a gantt row's bars — was z.array(z.any())
in the zod mirror and any[] on the TypeScript face. So this ordinary JSON was
green through validate:

{ "type": "timeline", "variant": "gantt",
  "items": [{ "label": "R", "items": [null] }] }

and the author's only signal came later, from the render-time date diagnostic:

Unusable gantt date range — items[0].items[0].startDate is undefined, which is
not a valid date.

That names the wrong fault. The bar is null; startDate is a key the author
never wrote. This is the mis-naming class objectui#7164 repaired for ROWS, one
level down.

The ruling this implements

Director seat, decision batch #71, 2026-09-07; maintainer reply verbatim,
unaltered and untranslated: 「其他同意」. Option B.

objectui#7164 narrowed the row and stopped at the bar level deliberately,
and wrote the stop down in both faces' docblocks. This PR supersedes that stop
knowingly, per the ruling — and removes the prose recording it rather than
qualifying it, so the next reader does not re-derive a gap that is now closed.
That re-derivation is how this card came to exist.

Option A is refused, not deferred. timeline.gantt.unusableRange.malformedRow's
copy is untouched, no fourth path level was added to that sentence, and the ten
language packs are untouched. The render-time date diagnostic remains the
defined outcome for anything that still reaches it — the renderer stays only
ever MORE lenient than validate, never the reverse.
packages/plugin-timeline/src/renderer.tsx was READ ONLY here; its row
predicate belongs to objectui#7364 and is not touched.

The change

Re-derived on the merged base c4b3750 (the ruling's line numbers were stale;
so were the ones taken at dispatch, because main moved again in between):

face file line on c4b3750 before after
mirror packages/types/src/zod/data-display.zod.ts :732 z.array(z.any()) z.array(z.object({}).passthrough())
TS packages/types/src/data-display.ts :1988 items?: any[] states the row/bar shape
note packages/types/src/zod/data-display.zod.ts :701 the objectui#7164 stop rewritten as superseded

⚠️ z.array(z.any()) occurs three times in that mirror; :243 and :258 are
data: on Table schemas and are NOT this target. The edit is anchored on the
enclosing TimelineRowSchema, not on a grep.

The TS face is the TypeScript spelling of the mirror at each level —
a Record KEYED BY string WITH unknown VALUES for the mirror's
z.object({}).passthrough(), and a string index signature for the passthrough
itself. (That type is spelled in words throughout this body, never in its
angle-bracket form — see the note at the foot.)

Nothing beyond the bar's object-ness is narrowed. A bar's own keys
(title / startDate / endDate / variant?) stay undeclared and open, so
items: [{}] still parses. TimelineSchema serves more than gantt, so this
was checked one variant over: a feed item carries no items key at all and is
untouched — pinned both ways in the new test.

Stock measurement — required, because this narrows a published accept set

Counted on 289d146 and re-measured unchanged on c4b3750, across apps/ ·
examples/ · content/ · packages/types/examples/:

timeline nodes  : 4
rows            : 11
bars TOTAL      : 5
bars WELL-FORMED (object) : 5   <-- FIRING CONTROL
bars null       : 0
bars non-object : 0

The zero is a reading, not an empty search. Two firing controls:

  1. The same walker reported the five well-formed bars it did find, with their
    paths.
  2. A planted null bar and a planted numeric bar, added to a scratch copy of
    the gantt fixture, were both found and named:
    examples/planted.json :: .items[0].items[2] and
    .items[1].items[2] (non-object: 7).

⇒ Nothing in this repository moves. ⚠️ hotcrm is a separate repository and is
not reachable from this checkout: it is unmeasured, and named as such rather
than counted as zero.

The docs page's gantt example is a TS literal, not JSON, so it is covered by a
different instrument — see the ablation below.

Evidence

Reverse verification of the TS face, against the rebuilt .d.ts a consumer
actually resolves (@object-ui/plugin-timeline has no src path alias, so it
reads dist/data-display.d.ts):

leg probe probe-attributed errors
MUTATION items: [null] 1 — TS2322: Type 'null' is not assignable to type 'Record KEYED BY string WITH unknown VALUES'
CONTROL a well-formed bar 0

The two legs' full error sets differ by exactly that one line. Restore proven by
an empty git diff HEAD, not by an exit code.

Ablation proving check:doc-snippets actually measures the docs gantt
example
— a green gate over 632 blocks does not by itself say this block is
one of them. Planting a null bar in content/docs/plugins/plugin-timeline.mdx
turned it red at exactly the expected place:

[semantic]  content/docs/plugins/plugin-timeline.mdx:215:9
            TS2322: Type 'null' is not assignable to type 'Record KEYED BY string WITH unknown VALUES'.
Semantic phase: 632 of 632 block(s) judged, 1 failed.

Restored and verified by blob hash 362f6d19ff9aba4656976c6cb870abab69e23f47
matching the HEAD blob, plus an empty git diff HEAD.

New pinpackages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts,
modelled on objectui#7164's. It carries the ruling's pin ({ items: [{ label: 'R', items: [null] }] } fails at items[0].items[0]; a well-formed bar
passes), asserts the refusal never mentions startDate, re-asserts objectui#7164's
row-level refusals as unchanged, and rebuilds the PREVIOUS declaration in-test
so the "before" column is measured against today's base rather than remembered.

Gates — all on the final head 630f54c, exit code captured before any pipe

gate exit verdict line
@object-ui/types build 0 dist completeness: 1 package(s) complete (124 emitted files verified)
@object-ui/types type-check 0 clean (--noEmit + tsconfig.examples.json + tsconfig.test.json)
@object-ui/types tests 0 Test Files 142 passed (142) · Tests 2723 passed (2723)
@object-ui/plugin-timeline type-check 0 clean
@object-ui/plugin-timeline tests 0 Test Files 25 passed (25) · Tests 303 passed (303)
@object-ui/types lint 0 272 problems (0 errors, 272 warnings) — 208 files, all warnings pre-existing
check:doc-snippets 0 Semantic phase: 632 of 632 block(s) judged, 0 failed
check:doc-types 0 Every documented component type is registered
check:doc-fences 0 every TypeScript block correctly fenced
check:spec-symbols 0 1353 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare
check:spec-floors 0 Every consumer-facing @objectstack/spec floor carries the symbols
check:readme-exports 0 43 tracked README(s) … 0 unbuilt
check:sdui-registration-pins 0 All 16 registration(s) … present in the built console
check:control-bytes 0 scanned 6656 tracked text file(s)
check:element-data-source-declaration 0 OK
check:handler-key-reads 0 OK
check:self-import 0 OK
check:phantom-deps 0 OK

Root test, lint and type-check run the whole farm and are CI's; the
package-scoped equivalents above are complete for this diff — @object-ui/types
is the only package whose source changed, and @object-ui/plugin-timeline is
the only consumer that reads the narrowed key.

Serial constraint, and the answer it produced

packages/types/src/zod/data-display.zod.ts is shared with PR #8354
(objectui#7760, draft, parked at the same gate), which edits TreeNodeSchema at
about :323. This card's region is the timeline row items at about :732.

That PR pins seven header figures on
packages/types/src/__tests__/zod-mirror-parity.test.ts, so the stop condition
was: does this change move any row there?

No. Zero rows added, removed or moved — the file is byte-identical to
origin/main (git diff origin/main -- packages/types/src/__tests__/zod-mirror-parity.test.ts
is empty), and it was not edited. Both faces narrowed symmetrically, so
there is no divergence to record: WiderThanDeclaredKeys still resolves to
never for TimelineSchema::items, which the parity test's type-level ledger
assertions confirm by passing under tsc -p tsconfig.test.json.

Contract review

Clause-② is yes — this narrows a published accept set: a null gantt bar is
legal today and will not be after this lands. needs:contract-review is hung on
this PR and on the card. This PR stays draft, is not enqueued, and has no
auto-merge armed; parking green is the intended end state.

Changeset: @object-ui/types minor. Breaking semantics, minor per this
repo's rule that its own breaking changes never declare major (the fixed group
follows @objectstack's cadence); the breaking half is spelled out in the
changeset body.

Notes

  • Related cards, deliberately not touched here and NOT acted on by this PR:
    objectui#7364 owns the renderer's row predicate, and objectui#7164 is the
    row-level card this one sits below. Both stay open; this PR acts on 7365 only.
  • ⛔ No HTML comment markers appear anywhere in this body, and no generic is
    written in its angle-bracket form — this platform's body sanitizer eats short
    tag-shaped fragments, including inside backticks and fenced blocks, so type
    arguments are spelled as words (Record KEYED BY string WITH unknown VALUES)
    and read literally in the source files, where they carry their real spelling.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX


Generated by Claude Code

`TimelineSchema.items[].items[]` — a gantt row's bars — was `z.array(z.any())`
in the zod mirror and `any[]` on the TypeScript face, so an authored `null` bar
was green through `validate` and only met the render-time date diagnostic,
which named `items[0].items[0].startDate is undefined` — a key the author never
wrote.

Both faces now declare the shape objectui#7164 declared one level up: the
mirror's row `items` becomes `z.array(z.object({}).passthrough())`, and the TS
face states the row/bar shape its docblock previously carried only in prose.
objectui#7164's deliberate stop at the row level is superseded knowingly, and
the prose recording that stop is removed rather than qualified.

The bar stays `.passthrough()` — its own keys are still undeclared — and the
render-time `malformedRow` copy and its ten language packs are untouched.

Narrows a published accept set. In-repo stock measured on 289d146 across
apps/ examples/ content/ packages/types/examples/: five authored bars, all
well-formed objects, zero null, positive-controlled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
…d on

The five-bar, zero-null in-repo reading was taken on 289d146 and re-taken
unchanged on c4b3750, the base this branch merges against. Cite both so the
figure in the docblocks, the pin header and the changeset matches the tree a
reviewer will check it against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BtWkWD9p.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.49KB 46.33KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.68KB 63.94KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

… the TS-face break

Contract review returned FAIL on two RECORD defects. Neither is a schema
change, and no existing assertion moved.

R1 — `timeline-items-row-shape-7164.test.ts` still carried objectui#7164's
"What is deliberately NOT narrowed" passage, stating that the bars stay
`z.any()` and that narrowing them is "a wider contract than the ruling named".
objectui#7365 supersedes that stop knowingly, so the file — the one the new
bar-level pin cross-references, and the one the mirror docblock names as the
row-level pin — now records the supersession instead of the superseded stop,
and points at `timeline-items-bar-shape-7365.test.ts`. The still-true half
(element and bar both stay `.passthrough()`; feed variants untouched; refining
by `variant` is still wider than either ruling named) is kept. No assertion in
the file was touched.

R2 — the changeset declared the `safeParse` narrowing in detail but not the
TypeScript face, which is the only record a downstream TypeScript consumer
reads. It now names that a row's and a bar's own keys type as `unknown` where
they were `any` (`TS2322: Type 'unknown' is not assignable to type 'string'`
at the read site), and that a `null` or non-object bar literal — and a `null`
row literal — no longer compiles. All five verified two-sidedly against a
rebuilt-in-place "previous face" control.

Also folds in the reviewer's optional, non-blocking suggestions on the 7365
pin, adding assertions only: every bar refusal is asserted to be an
`invalid_type` (an `unrecognized_keys` there would mean the wrong rule fired,
since the bar stays passthrough), and one case runs through the union door
`safeValidateSchema` that `objectui validate` actually applies, with the
well-formed half asserted beside it so a green refusal is not just "no arm
matched".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX

Copy link
Copy Markdown
Contributor Author

Patch round on the contract-review FAIL — two record edits, no schema change

Head 9f0d7a86, fast-forward from 630f54c3. Both findings reproduced; both are fixed as records only. ⛔ No schema change, no re-census, no existing assertion moved. packages/types/src/__tests__/zod-mirror-parity.test.ts is still untouched and blob-identical on both sides — git hash-object in this head's tree and git rev-parse origin/main:<that path> both give 612c8634c32ef6225681b643d549a7a1bb1689a0.

R1 — the superseded stop was still standing

packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts, reproduced with sed -n '46,52p'.

Before (on 630f54c):

 * ## What is deliberately NOT narrowed
 *
 * The bars inside a row stay `z.any()` and the element stays `.passthrough()`:
 * the two element shapes (feed item / gantt row) are discriminated by `variant`
 * and read dynamically, and a feed item carries no `items` key, so the feed
 * variants parse exactly as before. Narrowing the bar shape or refining by
 * `variant` is a wider contract than the ruling named.

After (same sed -n '46,52p' window on 9f0d7a86):

 * ## The bar level: a deliberate stop, since SUPERSEDED
 *
 * This card narrowed the ROW and stopped one level short DELIBERATELY — the
 * bars inside a row stayed `z.array(z.any())` — and this docblock recorded
 * that stop as the contract. ⭐ It is no longer the contract: objectui#7365
 * (director seat, decision batch #71, 2026-09-07, option B) SUPERSEDES the
 * stop KNOWINGLY. Every BAR is now `z.object({}).passthrough()` too, so a

The replacement runs to :67 and is two sections. The first, quoted above, continues: the null bar is refused at validate by its own name at items[i].items[j] instead of reaching the render-time date diagnostic that used to name a startDate the author never wrote; the bar level is pinned by ./timeline-items-bar-shape-7365.test.ts; and the table above it and every assertion below it are the ROW level and are unmoved by the bar narrowing. The second section, "What is still deliberately NOT narrowed, by either card", keeps the half that is still true — element and bar both stay .passthrough(), the two element shapes are discriminated by variant and read dynamically, a feed item carries no items key so the feed variants parse exactly as before, neither a row's nor a bar's own keys are declared, and refining by variant is still wider than either ruling named.

⛔ Prose only: 6 comment lines out, 19 comment lines in, zero assertions touched.

R2 — the changeset omitted the TypeScript-face break

Added to .changeset/gantt-bar-object-declaration.md, immediately after the safeParse paragraph and before the closing "A bar's own keys" one, verbatim:

**The TypeScript face breaks in the same direction, and it breaks at compile
time.** `TimelineSchema.items` was `any[]`; it is now an array of objects whose
`items` — the bars — is itself an array of objects. So a row's and a bar's own
keys type as `unknown` where they used to be `any`. Both of these compiled
before and are now `TS2322: Type 'unknown' is not assignable to type 'string'`
— annotate or narrow at the read site:

    const t: string = s.items[0].title;
    const d: string = s.items[0].items[0].startDate;

And a `null` or non-object bar LITERAL no longer compiles at all — nor does a
`null` row literal, which the `any[]` element used to admit.

(The two code lines sit in a real ts fence in the file; they are indented here only so this comment does not nest fences.)

Verified two-sidedly before the probe file was deleted. A throwaway test file rebuilt the PREVIOUS face in place — the head type with items?: any[] put back — and put five ts-expect-error directives on the head-face lines. tsc -p tsconfig.test.json exited 0: the five control lines compiled on the previous face, and all five directives were consumed (an unused one is itself an error), so every claim fired in both directions. Re-running with the directives turned off gave the messages, exit 2:

(22,11): error TS2322: Type 'unknown' is not assignable to type 'string'.
(24,11): error TS2322: Type 'unknown' is not assignable to type 'string'.
(26,99): error TS2322: Type 'null' is not assignable to type 'Record KEYED BY string WITH unknown VALUES'.
(28,99): error TS2322: Type 'number' is not assignable to type 'Record KEYED BY string WITH unknown VALUES'.
(30,77): error TS2322: Type 'null' is not assignable to type '{ [key: string]: unknown; items?: … }'.

Line 22 is s.items[0].title, line 24 is s.items[0].items[0].startDate, 26 is a null bar literal, 28 a numeric bar literal, 30 a null ROW literal — the last also breaks, because the previous any[] element admitted it. Probe deleted; git status clean and git diff HEAD empty before the commit. (Generics spelled in words here for the same reason the PR body gives; they carry their real spelling in the file.)

⭐ Optional suggestions, folded in — assertions ADDED, none changed

  • every bar refusal is asserted to be an invalid_type. An unrecognized_keys there would mean the wrong rule fired, since the bar deliberately stays .passthrough();
  • one case runs through the union door safeValidateSchema that objectui validate actually applies — a .passthrough() arm elsewhere in AnyComponentSchema could have re-admitted the document, so the door is measured rather than assumed, and the well-formed half is asserted beside it so a green refusal is not merely "no arm matched". The union reports its arms nested, so the path set is walked through per-arm errors and the refusal names items.0.items.0.

Gates on 9f0d7a86, exit captured before any pipe

gate exit verdict line
both pins (timeline-items-row-shape-7164 + timeline-items-bar-shape-7365) 0 Test Files 2 passed (2) · Tests 54 passed (54)
the 7365 pin, --reporter=verbose 0 Tests 31 passed (31); the three new cases named and green
pnpm --filter @object-ui/types type-check 0 tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — third leg included
node scripts/check-changeset-presence.mjs 0 ✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
eslint on the two edited test files 0 clean
control-byte self-scan on the three edited files no hits grep -naP over the C0/DEL ranges

origin/main was NOT merged

mergeable_state read clean immediately before the push, and git merge-tree --write-tree origin/main HEAD exited 0 against e5a28b9 (merge-base c4b3750) — no conflict to resolve, so the branch was left un-churned and the gates above sit on a head that is exactly the reviewed diff plus this patch.

⛔ PR stays draft. Not enqueued, no auto-merge, no approval, and needs:contract-review is untouched — the carrier stays until a fresh contract-tier review passes.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

⚠️ Erratum on the comment above, found by reading it back. The body sanitizer ate one character from it, twice — in the "verbatim" changeset quote and again in the prose below it. Both example reads are spelled in the file with a NON-NULL ASSERTION between items and the index: the token is items, then the non-null assertion operator, then [0]. That two-character sequence is reference-style image syntax in markdown and did not survive, inside the fence or outside it.

The file on disk carries the real spelling and is the authority — git show 9f0d7a86:.changeset/gantt-bar-object-declaration.md. Nothing else in that comment moved: the readback reached the footer intact, and the erratum is filed here rather than by rewriting the comment, since a rewrite of a body that is otherwise correct is the more expensive mistake.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BbGmQfuo.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.68KB 63.94KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants